Understanding Method Overriding, super(), and Special Methods in Python
Course: Python Training
Topic: Understanding Method Overriding, super(), and Special Methods
Course Link: Python Training Course
Demo Link: Register for a Course Demo
Website: JustAcademy
1. Introduction
Python supports object-oriented programming (OOP), which allows developers to organize programs using classes, objects, inheritance, encapsulation, and polymorphism. Three important concepts in object-oriented Python are method overriding, the super() function, and special methods.
Method overriding allows a child class to provide its own implementation of a method inherited from a parent class. The super() function helps access implementations from a parent class according to Python's method resolution order (MRO). Special methods, sometimes called magic methods or dunder methods, allow custom objects to work naturally with built-in operations such as printing, comparison, addition, iteration, and object creation.
These concepts help developers build flexible, reusable, and maintainable applications.
2. Learning Objectives
- Understand method overriding and why it is useful.
- Learn how inheritance supports method overriding.
- Use
super() to call parent-class implementations.
- Understand method resolution order in Python.
- Identify and implement common special methods.
- Differentiate between ordinary methods and special methods.
- Apply these concepts in practical Python programs.
- Recognize common errors and follow object-oriented programming best practices.
3. Understanding Inheritance in Python
Inheritance allows one class to reuse attributes and methods from another class. The class being inherited from is called the parent class, base class, or superclass. The class that inherits from it is called the child class, derived class, or subclass.
Syntax
class Parent:
def display(self):
print("This is the parent class")
class Child(Parent):
pass
obj = Child()
obj.display()
Output:
This is the parent class
Explanation: The Child class inherits from Parent. Because it does not define its own display() method, Python finds and executes the inherited implementation.
4. What Is Method Overriding?
Method overriding occurs when a subclass defines a method with the same name as an inherited method, providing a different or more specific implementation.
When the method is called on an instance of the child class, Python normally selects the implementation defined by the child class first.
Example: Basic Method Overriding
class Animal:
def sound(self):
print("Animals make different sounds")
class Dog(Animal):
def sound(self):
print("Dog barks")
animal = Animal()
dog = Dog()
animal.sound()
dog.sound()
Output:
Animals make different sounds
Dog barks
Explanation:
- The
Animal class defines the original sound() method.
- The
Dog class inherits from Animal.
- The
Dog class defines its own sound() method, overriding the inherited version.
- Calling
dog.sound() executes the child's implementation.
5. Why Is Method Overriding Important?
Method overriding is useful when different subclasses need different behavior while sharing a common interface.
- Customization: Subclasses can adapt inherited behavior to their specific requirements.
- Polymorphism: Different object types can respond differently to the same method call.
- Code reuse: Common functionality can remain in the parent class while specialized behavior is defined in subclasses.
- Extensibility: New subclasses can introduce different implementations without changing all the code that uses the shared interface.
- Maintainability: A well-designed class hierarchy separates general behavior from specialized behavior.
6. Method Overriding With Multiple Child Classes
Multiple subclasses can override the same parent method to provide different implementations. This is a common example of runtime polymorphism.
Example
class Shape:
def draw(self):
print("Drawing a shape")
class Circle(Shape):
def draw(self):
print("Drawing a circle")
class Rectangle(Shape):
def draw(self):
print("Drawing a rectangle")
class Triangle(Shape):
def draw(self):
print("Drawing a triangle")
shapes = [Circle(), Rectangle(), Triangle()]
for shape in shapes:
shape.draw()
Output:
Drawing a circle
Drawing a rectangle
Drawing a triangle
Explanation: Each object receives the same method call, draw(), but Python executes the implementation appropriate to that object's class.
7. Rules and Considerations for Method Overriding
- The subclass method generally uses the same name as the inherited method.
- The subclass can provide a different implementation.
- The subclass method should remain compatible with the parent method's expected interface when it is intended to be used polymorphically.
- Python does not require the overridden method to have exactly the same parameter list, but incompatible signatures can cause errors when callers use the parent interface.
- A child method can completely replace inherited behavior or extend it by calling
super().
- Overriding a method does not automatically call the parent implementation.
- Python method lookup follows the class's method resolution order.
8. What Is super() in Python?
The super() function provides a way to access the next implementation of a method in the method resolution order (MRO). It is commonly used in subclasses to extend parent-class behavior instead of replacing it entirely.
In ordinary single inheritance, super() is often used to call a parent class's method or constructor. In multiple inheritance, it follows the MRO rather than simply referring to one fixed parent class.
Basic Syntax
super().method_name()
For constructors, the common pattern is:
super().__init__(arguments)
9. Using super() to Call a Parent Method
When a child class overrides a method but still needs the original behavior, it can call super() inside the overridden method.
Example
class Animal:
def sound(self):
print("Animals can make sounds")
class Dog(Animal):
def sound(self):
super().sound()
print("Dog barks")
dog = Dog()
dog.sound()
Output:
Animals can make sounds
Dog barks
Explanation:
Dog overrides the sound() method.
super().sound() calls the next applicable implementation in the MRO.
- The child method then adds its own behavior.
This approach helps preserve reusable parent functionality while adding specialized behavior.
10. Using super() With Constructors
The __init__() method initializes an object. When a child class extends a parent class and the parent has initialization logic, the child can call super().__init__() to reuse it.
Example
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
class Student(Person):
def __init__(self, name, age, course):
super().__init__(name, age)
self.course = course
def display(self):
print("Name:", self.name)
print("Age:", self.age)
print("Course:", self.course)
student = Student("Rahul", 21, "Python")
student.display()
Output:
Name: Rahul
Age: 21
Course: Python
Explanation:
- The
Person constructor initializes name and age.
- The
Student constructor calls super().__init__(name, age) to reuse that initialization logic.
- The child constructor initializes the additional
course attribute.
- The student object contains all three attributes.
11. Extending Parent Behavior With super()
Method overriding does not require discarding the parent's implementation. A subclass can call the parent implementation and then perform extra operations.
Example: Employee Salary Display
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def display_details(self):
print("Employee:", self.name)
print("Salary:", self.salary)
class Manager(Employee):
def __init__(self, name, salary, department):
super().__init__(name, salary)
self.department = department
def display_details(self):
super().display_details()
print("Department:", self.department)
manager = Manager("Amit", 65000, "IT")
manager.display_details()
Output:
Employee: Amit
Salary: 65000
Department: IT
The manager reuses the employee's display logic and adds department information. This reduces duplicated code and makes the class easier to maintain.
12. Difference Between Method Overriding and super()
| Basis |
Method Overriding |
super() |
| Meaning |
A subclass provides its own implementation of an inherited method. |
Provides access to the next implementation in the MRO. |
| Purpose |
Customizes inherited behavior. |
Reuses or extends behavior from the inheritance chain. |
| Usage |
Define a method with the same name in a subclass. |
Call super().method_name(). |
| Parent behavior |
Not called automatically. |
Can be called explicitly through the MRO. |
| Constructor use |
A subclass may define its own __init__(). |
Often calls the next constructor implementation using super().__init__(). |
| Inheritance |
Commonly used in single or multiple inheritance. |
Especially useful in cooperative multiple inheritance. |
13. Understanding Method Resolution Order (MRO)
The method resolution order determines the order in which Python searches classes for an attribute or method. It is especially important when a class inherits from multiple parent classes.
Python uses the C3 linearization algorithm to determine the MRO for new-style classes, which includes all classes in modern Python.
Example
class A:
def show(self):
print("Class A")
class B(A):
def show(self):
print("Class B")
super().show()
class C(B):
def show(self):
print("Class C")
super().show()
obj = C()
obj.show()
print(C.mro())
Output from the method calls:
Class C
Class B
Class A
The MRO for the classes is C, then B, then A, followed by object. You can inspect the complete order using C.mro() or C.__mro__.
14. super() in Multiple Inheritance
Multiple inheritance occurs when a class inherits from more than one parent class. In these situations, super() is particularly useful because it follows the MRO, enabling classes to cooperate in a predictable sequence.
Example
class A:
def process(self):
print("Processing A")
class B(A):
def process(self):
print("Processing B")
super().process()
class C(A):
def process(self):
print("Processing C")
super().process()
class D(B, C):
def process(self):
print("Processing D")
super().process()
obj = D()
obj.process()
print([cls.__name__ for cls in D.mro()])
Output:
Processing D
Processing B
Processing C
Processing A
['D', 'B', 'C', 'A', 'object']
Explanation: The method calls follow the MRO of D. The call to super() in B continues to C, then to A. This cooperative pattern avoids hard-coding a particular parent class into each method.
Important: For cooperative multiple inheritance to work correctly, participating methods should generally use compatible signatures and consistently call super() where appropriate.
15. What Are Special Methods in Python?
Special methods are methods with names surrounded by double underscores, such as __init__, __str__, and __len__. They are also called dunder methods, short for “double underscore.”
These methods let custom classes define how objects behave with Python's built-in operations and language features.
For example:
__init__() initializes an object after it is created.
__str__() provides a user-friendly string representation.
__repr__() provides a developer-oriented representation.
__len__() supports the built-in len() function.
__add__() customizes behavior for the + operator.
__eq__() customizes equality comparisons.
__iter__() and __next__() support iteration protocols.
16. Understanding the __init__() Method
The __init__() method initializes an instance after it has been created. It is commonly used to assign initial values to instance attributes.
Example
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
book = Book("Python Basics", "Ravi Kumar")
print(book.title)
print(book.author)
Output:
Python Basics
Ravi Kumar
When Book() is called, Python creates the object and invokes __init__() to initialize it. Strictly speaking, object creation is handled by __new__(), while __init__() initializes the newly created instance.
17. Understanding the __str__() Method
The __str__() method defines the user-friendly string representation of an object. It is used by str() and generally by print().
Example
class Student:
def __init__(self, name, course):
self.name = name
self.course = course
def __str__(self):
return f"{self.name} is learning {self.course}"
student = Student("Amit", "Python")
print(student)
Output:
Amit is learning Python
Important: The __str__() method should return a string. Printing or returning another data type directly from this method can result in a TypeError.
18. Understanding the __repr__() Method
The __repr__() method provides a developer-oriented representation of an object. Ideally, the returned string is unambiguous and, where practical, can help recreate an equivalent object.
Example
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
def __repr__(self):
return f"Product(name={self.name!r}, price={self.price!r})"
product = Product("Laptop", 65000)
print(repr(product))
Output:
Product(name='Laptop', price=65000)
Difference between __str__() and __repr__():
| Feature |
__str__() |
__repr__() |
| Primary purpose |
Readable output for users. |
Informative output for developers. |
| Common usage |
print(obj) and str(obj). |
repr(obj) and interactive inspection. |
| Return value |
Must return a string. |
Must return a string. |
| Fallback |
If unavailable, Python generally falls back to __repr__(). |
If neither is defined, a default representation is generally used. |
19. Understanding __len__()
The __len__() method defines the result of calling len() on a custom object. It should return a non-negative integer representing the object's length.
Example
class Playlist:
def __init__(self, songs):
self.songs = songs
def __len__(self):
return len(self.songs)
playlist = Playlist(["Song A", "Song B", "Song C"])
print(len(playlist))
Output:
3
The built-in len() function calls the object's length protocol, allowing custom objects to behave like standard Python containers.
20. Understanding __add__()
The __add__() method defines behavior for the addition operator (+). It is useful when a custom class needs a meaningful way to combine two objects.
Example
class Number:
def __init__(self, value):
self.value = value
def __add__(self, other):
if not isinstance(other, Number):
return NotImplemented
return Number(self.value + other.value)
def __repr__(self):
return f"Number({self.value})"
num1 = Number(10)
num2 = Number(20)
result = num1 + num2
print(result)
Output:
Number(30)
Explanation: Python invokes the addition protocol when evaluating num1 + num2. The implementation returns a new Number object containing the sum.
Returning NotImplemented for unsupported operand types lets Python attempt the appropriate alternative operation or raise a suitable TypeError.
21. Understanding __eq__()
The __eq__() method customizes the behavior of the equality operator (==). It allows objects to be compared according to their meaningful data instead of only relying on their default identity-based behavior.
Example
class Student:
def __init__(self, student_id):
self.student_id = student_id
def __eq__(self, other):
if not isinstance(other, Student):
return NotImplemented
return self.student_id == other.student_id
student1 = Student(101)
student2 = Student(101)
student3 = Student(102)
print(student1 == student2)
print(student1 == student3)
Output:
True
False
Although student1 and student2 are separate objects, they compare equal because their student IDs match.
Note: Defining equality can affect hashing behavior. If objects are intended to be used as dictionary keys or set members, consider whether they should be hashable and how equality and __hash__() should work together.
22. Understanding __iter__() and __next__()
The methods __iter__() and __next__() support Python's iterator protocol. They are useful when creating custom objects that can be traversed using a for loop.
__iter__() returns an iterator.
__next__() returns the next item.
- When there are no more items,
__next__() raises StopIteration.
Example: Custom Iterator
class CountUp:
def __init__(self, limit):
self.limit = limit
self.current = 1
def __iter__(self):
return self
def __next__(self):
if self.current > self.limit:
raise StopIteration
value = self.current
self.current += 1
return value
counter = CountUp(3)
for number in counter:
print(number)
Output:
1
2
3
This custom iterator produces values from 1 through 3 and signals completion by raising StopIteration.
23. Understanding __call__()
The __call__() method allows an instance to be called using function-call syntax. This can be useful for callable objects that maintain configuration or state.
Example
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, number):
return number * self.factor
double = Multiplier(2)
print(double(5))
print(double(10))
Output:
10
20
Because Multiplier implements __call__(), the instance double can be used like a function.
24. Common Python Special Methods
| Special Method |
Purpose |
Common Trigger |
__new__() |
Creates and returns an instance. |
Object creation. |
__init__() |
Initializes an instance. |
After instance creation. |
__str__() |
Provides a readable string representation. |
str(obj), print(obj). |
__repr__() |
Provides a developer-oriented representation. |
repr(obj). |
__len__() |
Defines an object's length. |
len(obj). |
__add__() |
Defines addition behavior. |
obj1 + obj2. |
__sub__() |
Defines subtraction behavior. |
obj1 - obj2. |
__mul__() |
Defines multiplication behavior. |
obj1 * obj2. |
__eq__() |
Defines equality comparison. |
obj1 == obj2. |
__lt__() |
Defines less-than comparison. |
obj1 < obj2. |
__iter__() |
Returns an iterator. |
iter(obj) and iteration. |
__next__() |
Returns the next iterator item. |
next(iterator). |
__contains__() |
Defines membership behavior. |
item in obj. |
__call__() |
Makes an object callable. |
obj(). |
__enter__() |
Enters a context manager. |
with statement. |
__exit__() |
Handles context manager exit. |
When leaving a with block. |
25. Combining Method Overriding, super(), and Special Methods
The following example demonstrates inheritance, method overriding, parent initialization, and string representation in a single program.
class Person:
def __init__(self, name):
self.name = name
def describe(self):
return f"Person: {self.name}"
def __str__(self):
return self.describe()
class Student(Person):
def __init__(self, name, course):
super().__init__(name)
self.course = course
def describe(self):
parent_description = super().describe()
return f"{parent_description}, Course: {self.course}"
student = Student("Neha", "Python")
print(student.describe())
print(student)
Output:
Person: Neha, Course: Python
Person: Neha, Course: Python
Explanation:
Person defines the common describe() method.
Student overrides describe() to include course information.
super().__init__(name) reuses the parent constructor.
super().describe() reuses the parent's description.
__str__() provides a readable representation when the student object is printed.
26. Common Mistakes and How to Avoid Them
Mistake 1: Forgetting to Call the Parent Constructor
class Parent:
def __init__(self, name):
self.name = name
class Child(Parent):
def __init__(self, name, age):
self.age = age
Problem: The child constructor does not initialize self.name through the parent constructor.
Correction:
class Child(Parent):
def __init__(self, name, age):
super().__init__(name)
self.age = age
Mistake 2: Assuming Overriding Automatically Calls the Parent Method
A method defined in a child class replaces the inherited method for normal lookup on that child instance. If the parent's behavior is needed, call it explicitly using super().
Mistake 3: Using an Incorrect super() Call
class Parent:
def display(self):
print("Parent")
class Child(Parent):
def display(self):
super().display()
print("Child")
In this example, super().display() is the correct way to continue to the next implementation in the MRO. Avoid hard-coding a parent class unnecessarily, particularly in cooperative multiple inheritance.
Mistake 4: Returning the Wrong Type From a Special Method
Methods such as __str__() and __repr__() must return strings. A method such as __len__() must return a non-negative integer. Follow each special method's protocol.
Mistake 5: Using Incompatible Method Signatures
If a child overrides a method but changes its parameters incompatibly, code that expects the parent interface may fail. Keep signatures compatible when polymorphic substitution is intended.
Mistake 6: Ignoring the MRO in Multiple Inheritance
In multiple inheritance, super() follows the MRO rather than simply calling one particular parent. Inspect ClassName.mro() when investigating unexpected method-call order.
27. Best Practices
- Use method overriding when a subclass needs specialized behavior.
- Use
super() when extending inherited behavior or participating in cooperative multiple inheritance.
- Call
super().__init__() when the parent's initialization is needed.
- Maintain compatible method signatures when subclasses should be interchangeable with their parent classes.
- Use special methods to implement meaningful behavior for Python operations rather than for decoration.
- Ensure
__str__() and __repr__() return strings.
- Return
NotImplemented from binary comparison or arithmetic methods when an operand type is unsupported, where appropriate.
- Use
ClassName.mro() to understand the order of method lookup.
- Write small test cases for inherited behavior, overridden behavior, and special methods.
- Prefer clear class design over deep or unnecessarily complicated inheritance hierarchies.
28. Practice Exercises
Exercise 1: Animal Sound
Create an Animal class with a sound() method. Create Dog and Cat subclasses that override it with their respective sounds.
Exercise 2: Employee and Manager
Create an Employee class that initializes a name and salary. Create a Manager subclass that adds a department. Use super() to initialize inherited attributes.
Exercise 3: Product String Representation
Create a Product class with name and price attributes. Implement __str__() so that printing an object displays its name and price clearly.
Exercise 4: Custom Length
Create a Team class that stores a list of members. Implement __len__() to return the number of members.
Exercise 5: Custom Addition
Create a Money class that stores an amount. Implement __add__() to add two compatible Money objects and return a new object.
Exercise 6: Multiple Inheritance
Create classes A, B, and C with a shared method. Create a class inheriting from both B and C, use super() cooperatively, and inspect its MRO.
29. Interview Questions and Answers
Q1. What is method overriding in Python?
Answer: Method overriding occurs when a subclass defines its own implementation of a method inherited from a parent class.
Q2. What is the purpose of super()?
Answer: super() provides access to the next implementation of a method in the method resolution order. It is often used to reuse parent-class initialization and behavior.
Q3. Does overriding automatically call the parent method?
Answer: No. If the parent implementation is needed, the subclass must call it explicitly, commonly through super().
Q4. What is the difference between __init__() and __new__()?
Answer: __new__() creates and returns an instance, while __init__() initializes the instance after creation.
Q5. What are dunder methods?
Answer: Dunder methods are special methods with names beginning and ending with double underscores, such as __str__() and __len__().
Q6. What is the difference between __str__() and __repr__()?
Answer: __str__() focuses on readable user-facing output, while __repr__() focuses on an informative developer-facing representation.
Q7. What is method resolution order?
Answer: MRO is the order in which Python searches classes for methods and attributes. It is especially important in multiple inheritance.
Q8. Can special methods be overridden?
Answer: Yes. A custom class can implement or override special methods to define how its objects interact with built-in operations and language features.
Q9. What happens when a special method returns an invalid value?
Answer: Python may raise a TypeError or another exception if the method violates its protocol, such as returning a non-string from __str__().
Q10. Why is super() useful in multiple inheritance?
Answer: It follows the MRO, allowing cooperating classes to call the next implementation in a consistent order without hard-coding a particular parent class.
30. Quick Revision Checklist
- Method overriding customizes inherited methods.
- Inheritance enables subclasses to reuse parent-class behavior.
super() calls the next implementation in the MRO.
super().__init__() is commonly used to reuse constructor initialization.
- Python uses the C3 linearization algorithm to determine MRO.
- Special methods connect custom objects with built-in Python operations.
__init__() initializes objects, while __new__() creates them.
__str__() and __repr__() return string representations.
__len__() implements the length protocol.
__add__() and __eq__() customize arithmetic and equality.
__iter__() and __next__() implement iterator behavior.
- Test method signatures, MRO, and special-method return values carefully.
31. Conclusion
Method overriding, super(), and special methods are essential parts of Python object-oriented programming. Method overriding lets subclasses customize inherited behavior, super() enables classes to reuse and extend methods according to the method resolution order, and special methods allow custom objects to work naturally with Python's built-in language features.
Understanding these concepts helps you design extensible class hierarchies, reduce duplicated code, implement polymorphism, and create custom objects with intuitive behavior. Practice these examples by creating your own classes and testing how inherited methods and special methods work together.
Continue your Python learning journey with the JustAcademy Python Training Course, visit the JustAcademy website, or register for a course demo.